package edu.cmu.ece.ds.droidchat.model;

import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;

import android.util.Log;
import edu.cmu.ece.ds.droidchat.message.ChatSessionMessage;
import edu.cmu.ece.ds.droidchat.util.Toolbox;

/*
 * this is the data structure that records all the information of a chat session
 * each user should keep one copy of this when enter or create a chat session
 */

public class ChatSessionInfo {

    public class ParticipantLife{
        private String username;
        private String IP;
        private int ttl;
        ParticipantLife(String username,String IP)
        {
            this.username=username;
            this.IP=IP;
            ttl=10;
        }
        void decreaseTTL()
        {
            ttl--;
            if(ttl<0){
                removeParticipant(username);
                addParticipantsLeft(username);
            }
        }
        void killTTL()
        {
            ttl=1;
        }
		void increaseTTL()
        {
            ttl=10;
        }
        public String getUsername() {
            return username;
        }
        public String getIP() {
            return IP;
        }
        public void setIP(String ip){
        	this.IP = ip;
        }
    }

    private static final String TAG = "ChatSessionInfo";

    /*
     * following three are for keys actually symmetric key.
     */
    private static String algorithm = "DESede";

    // used to decrypt/encrypt chat messages
    // for this chat session
    private Cipher cipher = null;

    // chat session id, different from login session id....
    // identifies a chat session
    private String sessionid;

	// key = the username of all the participants
    // value = the ip of the participant
    private HashMap<String, ParticipantLife> participants;

    // key = the username of the participant
    // value = the latest seq num of the participant
    public HashMap<String, Integer> participantsSeqNum;
    
    // key = the username of the participant
    // value = the latest kl num received from the participant
    public HashMap<String, Integer> participantsKLNum;

    // a list of participants that you just detected to have moved
    //  used in ChatWindowActivity to display system message that
    //  "XX has left the chat session"
    private HashSet<String> participantsLeft;
    
    public HashSet<String> nonResponsive;
    
    private boolean pendingForMyInvitation = true;
    private boolean pendingForMyResponse = false;

    // the username of the latest invitee
    private String username;

    // the username of the inviter
    private String inviter;

    // the latest invitee's user's public key
    private BigInteger publicModulus;
    private BigInteger publicExponent;

    private boolean isInviting = false;
    private boolean invitationSent = false;

    private boolean dead = false;

	private int mySeqNum = 0;

    private int myKLRound = 0;
    private int t = 0;

	// useful information to NearByUserListActivity
    //  set in addNewOneOnOneChatSessionInfoByUsername() in Toolbox
    private boolean brandNew = false;

	private ArrayList<ChatMessage> messagesDisplayed;
	// messages received but not yet consumed by ChatWindowActivity
    private ArrayList<ChatMessage> messagesReceived;

    /*
     * for creator, we don't have keys, so constructor will create that for
     * others, they will receive those things and no need to create
     */
    public ChatSessionInfo(String selfUsername, LoginSessionAttribute login) {

        try {
            this.key = KeyGenerator.getInstance(algorithm).generateKey();
            this.cipher = Cipher.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        }

        if (login != null) {
            this.sessionid = login.getUsername() + login.getChatsessionnum();
        } else {
            // for Dennis facebook
            this.sessionid = Double.toString(Math.random());
        }
        // participants = new ArrayList<String>();
        // participants.add(selfUsername);
        pendingForMyInvitation = false;
        pendingForMyResponse = false;
        participants = new HashMap<String, ParticipantLife>();
        participantsSeqNum = new HashMap<String, Integer>();
        messagesReceived = new ArrayList<ChatMessage>();
    }

    public ChatSessionInfo(LoginSessionAttribute login) {
        this.key = null;
        this.cipher = null;
        if (login != null) {
            this.sessionid = login.getUsername() + login.getChatsessionnum();
        } else {
            // for Dennis facebook
            this.sessionid = Double.toString(Math.random());
        }
        participants = new HashMap<String, ParticipantLife>();
        // participants = new ArrayList<String>();
        // participants.add("null");
    }

    /**
     * inviter calls this method to create a new chat session a new chat session
     * id is generated
     *
     * set pending to true
     */
    public ChatSessionInfo() {
        this.key = null;
        this.cipher = null;
        UserInfo myInfo = Toolbox.getMyInfo();
        if (myInfo != null) {
            this.sessionid = myInfo.getUsername() + myInfo.getChatSessionNum();
        }
        pendingForMyInvitation = true;
        pendingForMyResponse = false;
        participants = new HashMap<String, ParticipantLife>();
        participantsSeqNum = new HashMap<String, Integer>();
        participantsKLNum = new HashMap<String, Integer>();
        messagesReceived = new ArrayList<ChatMessage>();
		messagesDisplayed = new ArrayList<ChatMessage>();
		nonResponsive = new HashSet<String>();
		participantsLeft = new HashSet<String>();
    }

    /**
     * invitee calls this method to create a new chat session chat session id is
     * not generated
     */
    public ChatSessionInfo(String inviterChatSessionID,
            String inviter) {
        this.key = null;
        this.cipher = null;
        /*UserInfo myInfo = Toolbox.getMyInfo();
        if (myInfo != null) {*/
        this.sessionid = inviterChatSessionID;
        //}

        this.inviter = inviter;

        pendingForMyInvitation = false;
        pendingForMyResponse = true;
        participants = new HashMap<String, ParticipantLife>();
        participantsSeqNum = new HashMap<String, Integer>();
        participantsKLNum = new HashMap<String, Integer>();
        messagesReceived = new ArrayList<ChatMessage>();
		messagesDisplayed = new ArrayList<ChatMessage>();
		nonResponsive = new HashSet<String>();
		participantsLeft = new HashSet<String>();
    }

    /*
     * This constructor is intended to be called by the chat session creator.
     * Chat session key is generated
     */
    /*
     * public ChatSessionInfo(String id,String self,String receiver) throws
     * NoSuchAlgorithmException, NoSuchPaddingException{ this.key =
     * KeyGenerator.getInstance(algorithm).generateKey(); this.cipher =
     * Cipher.getInstance(algorithm); this.sessionid=id; participants=new
     * ArrayList<String>(); participants.add(self); participants.add(receiver);
     * }
     */

    /*
     * This constructor is intended to be called by the the users being invited
     * to join a chat session. Chat session key not generated
     */
    /*
     * public ChatSessionInfo(Key key, Cipher cipher,String id) throws
     * NoSuchAlgorithmException, NoSuchPaddingException{ if(key!=null &&
     * cipher!=null) { this.key=key; this.cipher=cipher; } this.sessionid=id;
     * participants=new ArrayList<String>(); }
     */

    /*
     * following are encrypt and decrypt functions given a plaintext or cypher,
     * return the other thing
     */

    public void generateSessionKey()
    {
    	if(this.key ==null && this.cipher ==null)
    	{
	        try {
	            this.key = KeyGenerator.getInstance(algorithm).generateKey();
	            this.cipher = Cipher.getInstance(algorithm);
	            System.out.println("                the key is "+ key.toString());
	        } catch (NoSuchAlgorithmException e) {
	            e.printStackTrace();
	        } catch (NoSuchPaddingException e) {
	            e.printStackTrace();
	        }
    	}
    }
    
    public byte[] encrypt(String input) throws InvalidKeyException,
            BadPaddingException, IllegalBlockSizeException {
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] inputBytes = input.getBytes();
        return cipher.doFinal(inputBytes);
    }

    public String decrypt(byte[] encryptionBytes) throws InvalidKeyException,
            BadPaddingException, IllegalBlockSizeException {
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] recoveredBytes = cipher.doFinal(encryptionBytes);
        String recovered = new String(recoveredBytes);
        return recovered;
    }

    public Key getKey() {
        return key;
    }

    public Cipher getCipher() {
        return cipher;
    }

    // returns the chat session id for this chat session
    public String getSessionid() {
        return sessionid;
    }

    public BigInteger getPublicModulus() {
        return publicModulus;
    }

    public BigInteger getPublicExponent() {
        return publicExponent;
    }

    // get the latest user that is being invited
    public String getUsername() {
        return username;
    }

    // get the username of the invitor (if available)
    public String getInviter() {
        return inviter;
    }
    // set the username of the inviter
    public void setInviter(String inviter) {
        this.inviter = inviter;
    }

    public void setPublicExponent(BigInteger publicExponent) {
        this.publicExponent = publicExponent;
    }

    public void setPublicModulus(BigInteger publicModulus) {
        this.publicModulus = publicModulus;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    private Key key = null; // the chat session key,

    public boolean isPendingForMyInvitation() {
        return pendingForMyInvitation;
    }

    public void setPendingForMyInvitation(boolean pending) {
        this.pendingForMyInvitation = pending;
    }

    public boolean isPendingForMyResponse() {
        return pendingForMyResponse;
    }

    public void setPendingForMyResponse(boolean pendingForMyResponse) {
        this.pendingForMyResponse = pendingForMyResponse;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public void setCipher(Cipher cipher) {
        this.cipher = cipher;
    }

    /*
     * return a deep copy, so that ConcurrentModificationException
     *  does not show up...
     */
    public HashMap<String, ParticipantLife> getParticipants() {
    	HashMap<String, ParticipantLife> deepCopy = 
    		new HashMap<String, ChatSessionInfo.ParticipantLife>(participants);
    	
    	Log.e("getParticipants()", "getParticipants()");
    	
    	for(String username: deepCopy.keySet()){
    		Log.e("getParticipants()", "username=" + username);
    		Log.e("getParticipants()", "ip=" + deepCopy.get(username).getIP());
    	}
    	
    	return deepCopy;
    }
    
    /*
     * return the number of participants
     */
    public int numberOfParticipants(){
    	return participants.keySet().size();
    }

    /*
     * public void setParticipants(ArrayList<String> participants) {
     * this.participants = participants; }
     */

    public String getChatSessionName() {
        String ret = participantsToString();
        if (ret.length() != 0 && (username != null || inviter != null)) {
            ret += "; ";
        }
        if (username != null) {
            ret += username;
        }
        if (inviter != null){
        	ret += inviter;
        }
        
        if(username != null){
        	Log.e(TAG, "participants=" + participantsToString() + ",pending invitee="
                + username);
        }

        if(inviter != null){
        	Log.e(TAG, "participants=" + participantsToString() + ",the inviter is "
                + inviter);
        }
        
        if(ret.equals("")){
        	return "dead chat session";
        }
        
        return ret;
    }

    public void addParticipant(String username, String IP) {
        Log.e(TAG, "addParticipant()");
    	this.participants.put(username, new ParticipantLife(username,IP));
        
        // 0 means no chat message from username received yet
        this.participantsSeqNum.put(username, 0); 
        
        // 0 means no keep alive received from username received yet
        this.participantsKLNum.put(username, 0);
        
        HashMap<String, ParticipantLife> deepCopy = 
    		new HashMap<String, ChatSessionInfo.ParticipantLife>(participants);
    	
    	Log.e("addParticipants()", "addParticipants()");
    	
    	for(String u: deepCopy.keySet()){
    		Log.e("getParticipants()", "username=" + u);
    		Log.e("getParticipants()", "ip=" + deepCopy.get(u).getIP());
    	}
        
    }
    
    public void setParticipantIP(String reporter, int reporterIP) {
		String newIP = Toolbox.intToIp(reporterIP);
		if(participants.containsKey(reporter)){
			ParticipantLife p = participants.get(reporter);
			if(!p.getIP().equals(newIP)){
				p.setIP(newIP);
			}
		}
	}
    
    public boolean isAParticipant(String username) {
		return participants.containsKey(username);
	}
    
    private void removeParticipant(String username)
    {
        System.out.println("we are now removing :" + username);
        if(this.participants.containsKey(username)){
            this.participants.remove(username);
            this.participantsSeqNum.remove(username);
            this.participantsKLNum.remove(username);
            //this.nonResponsive.remove(username);
        }

        // nobody left, remove this session from hash table!
        if(this.participants.size()==0){
            //Toolbox.removeChatSession(this.sessionid);
        	Toolbox.removeChatSession(this.sessionid, false);
        }
    }

    public void increasettl(String username)
    {
        ParticipantLife parLife;
        if(participants.containsKey(username))
        {
            parLife=participants.get(username);
            parLife.increaseTTL();
        }
    }

    public void decreasettl(String username)
    {
        ParticipantLife parLife;
        if(participants.containsKey(username))
        {
            parLife=participants.get(username);
            parLife.decreaseTTL();
        }
    }
    
    public void killttl(String username)
    {
        ParticipantLife parLife;
        if(participants.containsKey(username))
        {
            parLife=participants.get(username);
            parLife.killTTL();
        }
    }

    public boolean isInviting() {
        return isInviting;
    }

    public void setIsInviting(boolean isInviting) {
        this.isInviting = isInviting;
    }

    public boolean InvitationSent() {
        return invitationSent;
    }

    public void setInvitationSent(boolean invitationSent) {
        this.invitationSent = invitationSent;
    }

    public int getMyNextSeqNum() {
        return ++mySeqNum;
    }

    public int getT(){
    	return t;
    }
    public void setT(int t) {
		this.t = t;
	}
    
    /**
     * returns true
     *
     * if the message is the next message expected sequence number = old
     * sequence number + 1
     *
     * OR
     *
     * if the message has been delivered sequence number <= old sequence number
     *
     */
    public boolean addNewMessageReceived(ChatSessionMessage m) {
        UserInfo u = Toolbox.retrieveUserInfoByUsername(m.getSenderUsername());
        
        Log.e(TAG, "addNewMessageReceived()");
        
        int senderSeqNum = m.getSenderSeqNum();
        String sender = m.getSenderUsername();
        boolean result = false;
        synchronized(messagesReceived){
        if (participantsSeqNum.containsKey(sender)) {
            if (senderSeqNum == participantsSeqNum.get(sender) + 1) {
            	Log.e("addNewMessageReceived()", "bigger by 1");
                ChatMessage msg = new ChatMessage(u, m.getMessage(),
                        m.getChatSessionID());
				msg.setSeqNum(m.getSenderSeqNum());
                msg.setT(m.getT());
                messagesReceived.add(msg);
                participantsSeqNum.put(sender, senderSeqNum);
				updateTuponReceive(msg);
                //return true;
				result = true;
            } else if (senderSeqNum <= participantsSeqNum.get(sender)) {
            	Log.e("addNewMessageReceived()", "less than equal to");
                // something I've already received
            	// should probably check if it's really in 'messagesReceived'
            	boolean found = false;
            	synchronized(messagesDisplayed){
            		Iterator<ChatMessage> itr = messagesDisplayed.iterator();
            		while(itr.hasNext()){
            			ChatMessage mm = itr.next();
            			if(mm.isSystemMsg()==false){
            				if(mm.getUserinfo().getUsername().equals(m.getSenderUsername())){
            					if(mm.getSeqNum()==m.getSenderSeqNum()){
            						if(mm.getT()==m.getT()){
            							found=true;
            						}
            					}
            				}
            			}
            		}

        			Iterator<ChatMessage> itr2 = messagesReceived.iterator();
            		while(itr2.hasNext()){
            			ChatMessage mm = itr2.next();
            			if(mm.isSystemMsg()==false){
            				if(mm.getUserinfo().getUsername().equals(m.getSenderUsername())){
            					if(mm.getSeqNum()==m.getSenderSeqNum()){
            						if(mm.getT()==m.getT()){
            							found=true;
            						}
            					}
            				}
            			}
            		}
            		
            		if(found==false){
            			ChatMessage msg = new ChatMessage(u, m.getMessage(),
                                m.getChatSessionID());
        				msg.setSeqNum(m.getSenderSeqNum());
                        msg.setT(m.getT());
                    	messagesReceived.add(msg);
            		}
            	}
            	Log.e("addNewMessageReceived()", "found=" + found);
                //return true;
            	result = true;
            } else if (participantsSeqNum.get(sender)==0 && senderSeqNum > participantsSeqNum.get(sender)) {
            	Log.e("addNewMessageReceived()", "special case");
            	// temporary hack - just to see if the multi-user message can be delivered
            	ChatMessage msg = new ChatMessage(u, m.getMessage(),
                        m.getChatSessionID());
				msg.setSeqNum(m.getSenderSeqNum());
                msg.setT(m.getT());
                messagesReceived.add(msg);
                participantsSeqNum.put(sender, senderSeqNum);
				updateTuponReceive(msg);
            	//return true;
				result = true;
            }
        }
        }
        //return false;
        return result;
    }
	
    private void updateTuponReceive(ChatMessage msg) {
		int currentT = this.t;
		int messageT = msg.getT();
		
		this.t = Math.max(currentT, messageT)+1;
	}
	public int incrementCurrrentT(){
    	return ++this.t;
    }

    public int getNewMessagesCount() {
    	int x = 0;
    	synchronized(messagesReceived){
    		x = messagesReceived.size();
    	}
        return x;
    }

    public ArrayList<ChatMessage> getNewMessages() {
        @SuppressWarnings("unchecked")
        ArrayList<ChatMessage> clone = (ArrayList<ChatMessage>) messagesReceived
                .clone();
        synchronized(messagesReceived){
	        synchronized(messagesDisplayed){
	        	messagesDisplayed.addAll(messagesReceived);
	        }
	        messagesReceived.clear();
        }
        Collections.sort(clone);
        return clone;
    }

	public ArrayList<ChatMessage> getMessagesDisplayed(){
    	
		ArrayList<ChatMessage> clone = new ArrayList<ChatMessage>(messagesDisplayed);
		Collections.sort(clone);
		
    	for(ChatMessage m: clone){
    		Log.d("getMessagesDisplayed", m.toString());
    	}
    	
    	return clone;
    }
	public void addMyMessageDisplayed(ChatMessage message) {
		synchronized(messagesDisplayed){
			messagesDisplayed.add(message);
		}
	}

	
    /**
     * returns the list of participants in sorted order separated by ',' sorting
     * haven't done yet... to be added
     */
    public String participantsToString() {
        StringBuffer str = new StringBuffer();

        ArrayList<String> toBeSorted = new ArrayList<String>();

        for (String username : participants.keySet()) {
            toBeSorted.add(username);
            // str.append(username);
            // str.append(",");
        }

        // sort the ArrayList of Strings
        Collections.sort(toBeSorted);

        if (toBeSorted.isEmpty()) {
            return "";
        }

        for (String username : toBeSorted) {
            str.append(username);
            str.append(",");
        }

        return str.subSequence(0, str.length() - 1).toString();
    }

    public boolean isBrandNew() {
		return brandNew;
	}

	public void setBrandNew(boolean brandNew) {
		this.brandNew = brandNew;
	}
    
    public String toString() {
        return "ChatSessionID=" + this.sessionid + " number of participants="
                + this.participants.keySet().size();
    }

    // is the given user in the participant list of the chat session
	public boolean hasParticipant(String username) {
		return participants.containsKey(username);
	}
	
	public int getMyNextKLRound(){
		return ++myKLRound;
	}

	// return the latest KL round number reported by username
	public int getParticipantLatestKLRound(String username) {
		return participantsKLNum.get(username);
	}

	public void setParticipantLatestKLRound(String reporter, int reporterRound) {
		int current = participantsKLNum.get(reporter);
		
		if(reporterRound > current){
			participantsKLNum.put(reporter, reporterRound);
		}
	}
	
	public boolean isNotResponsiveParticipant(String username){
		boolean result = false;
		synchronized(nonResponsive){
			if(nonResponsive.contains(username)){
				result = true;
			}
		}
		return result;
		
		/*if(nonResponsive.contains(username)){
			return true;
		}
		return false;*/
	}

	public void setParticipantNonResponsive(String username) {
		synchronized(nonResponsive){
			nonResponsive.add(username);
		}
	}
	public void removeParticipantNonResponsive(String username) {
		synchronized(nonResponsive){
			if(nonResponsive.contains(username)){
				nonResponsive.remove(username);
			}
		}
	}
	
	private void addParticipantsLeft(String username) {
		synchronized(participantsLeft){	
			if(!participantsLeft.contains(username)){
				participantsLeft.add(username);
			}
		}
	}
	public HashSet<String> getParticipantLeft(){
		HashSet<String> clone = null;
		synchronized(participantsLeft){	
			clone = new HashSet<String>(participantsLeft);
			participantsLeft.clear();
		}
		return clone;
	}
	
	public boolean isDead() {
		return dead;
	}
	public void setDead(boolean dead) {
		this.dead = dead;
	}
	
	public void addSystemMessageNotResponsive(ChatSessionMessage message, String receiver) {
		ChatMessage sysMessage = new ChatMessage(
				Toolbox.getMyInfo(),
				receiver + " is NOT reponsive",
				message.getChatSessionID());
		
		sysMessage.setT(message.getT());
		sysMessage.setSeqNum(message.getSenderSeqNum());
		sysMessage.setSystemMsg(true);
		sysMessage.setSysMsgEnd(false);
		synchronized(messagesDisplayed){
			messagesDisplayed.add(sysMessage);
		}
	}

	public void addSystemMessageNowResponsive(ChatSessionMessage message,
			String ackUsername) {
		ChatMessage sysMessage = new ChatMessage(
				Toolbox.getMyInfo(),
				ackUsername + " is NOW reponsive",
				message.getChatSessionID());
		
		sysMessage.setT(message.getT());
		sysMessage.setSeqNum(message.getSenderSeqNum());
		sysMessage.setSystemMsg(true);
		sysMessage.setSysMsgEnd(true);
		synchronized(messagesDisplayed){
			messagesDisplayed.add(sysMessage);
		}
	}
	
	public void addSystemMessageNowLeft(String leaver){
		ChatMessage myLastSentMsg = null;
		UserInfo me = Toolbox.getMyInfo();
		synchronized(messagesDisplayed){
			for(int i=messagesDisplayed.size()-1; i>=0; i--){
				ChatMessage m = messagesDisplayed.get(i);
				if(!m.isSystemMsg()){
					if(m.getUserinfo().getUsername().equals(me.getUsername())){
						myLastSentMsg = m;
						break;
					}
				}
			}
		}
		ChatMessage sysMessage = new ChatMessage(
				Toolbox.getMyInfo(),
				leaver + " has NOW left the chat session",
				this.sessionid);
		sysMessage.setT(myLastSentMsg.getT());
		sysMessage.setSeqNum(myLastSentMsg.getSeqNum());
		sysMessage.setSystemMsg(true);
		sysMessage.setSysMsgEnd(true);
		synchronized(messagesDisplayed){
			messagesDisplayed.add(sysMessage);
		}
	}
	
    public void setSessionid(String sessionid) {
		this.sessionid = sessionid;
	}
}
